package com.wedimob.slideshare.parser.annotation;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import com.wedimob.slideshare.bo.Container;
import com.wedimob.slideshare.bo.Medium;
import com.wedimob.slideshare.source.Source;

public class Factory {
	static public Source getSource(Document doc) {
		for (Source src : Source.values()) {
			if (doc.baseUri().contains(src.getStrategy().getUrlBase()))
				return src;
		}

		return null;
	}

	static public URL buildURL(Document doc, String url) {
		if (url == null)
			return null;

		try {
			if (url.startsWith("http"))
				return new URL(url);
			if (url.startsWith("//"))
				return new URL("http:" + url);
			return new URL(getSource(doc).getStrategy().getUrlBase() + url);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	static public Calendar parseCalendar(String calendar) {
		if (calendar == null)
			return null;

		// minutes
		int unit = 1;

		if (calendar.contains("hour") || calendar.contains("heure"))
			unit = 60;

		if (calendar.contains("day") || calendar.contains("jour"))
			unit = 24 * 60;

		if (calendar.contains("week") || calendar.contains("semaine"))
			unit = 7 * 24 * 60;

		if (calendar.contains("month") || calendar.contains("mois"))
			unit = 30 * 24 * 60;

		if (calendar.contains("year") || calendar.contains("an"))
			unit = 365 * 24 * 60;

		Calendar result = Calendar.getInstance();

		if (unit == 1) {
			try {
				result.setTime(DateParser.parse(calendar));
				return result;
			} catch (Exception e) {
				try {
					SimpleDateFormat formatter = new SimpleDateFormat(
							"EEE MMM d HH:mm:ss Z yyyy", Locale.ENGLISH);
					result.setTime(formatter.parse(calendar));
				} catch (ParseException e1) {
					return null;
				}
			}
		}

		Integer quantity = parseInteger(calendar);

		if (quantity == null)
			return null;

		result.add(Calendar.MINUTE, -1 * quantity * unit);

		return result;
	}

	static final private Pattern intPattern = Pattern.compile("-?\\d+");

	static public Integer parseInteger(String number) {
		if (number == null)
			return null;

		number = number.replace(",", "");
		Matcher m = intPattern.matcher(number);
		while (m.find()) {
			return Integer.parseInt(m.group());
		}

		return null;
	}

	static final Pattern doublePattern = Pattern.compile("C=(\\d+\\.\\d+)");

	static private Double parseDouble(String number) {
		if (number == null)
			return null;

		number = number.replace(",", "");
		Matcher m = doublePattern.matcher(number);
		while (m.find()) {
			return Double.parseDouble(m.group());
		}

		return null;
	}

	static private Elements getElements(Document doc, String pattern) {
		if (doc == null)
			return null;

		return doc.select(pattern);
	}

	static private Element getFirstElement(Document doc, String pattern) {
		Elements elemts = getElements(doc, pattern);
		if (elemts == null)
			return null;
		if (elemts.size() >= 1)
			return elemts.first();

		return null;
	}

	static public Document createDocument(Element elemt, String baseUri) {
		Document newDoc = new Document(baseUri);
		newDoc.appendElement(elemt.tagName()).append(elemt.html());
		return newDoc;
	}

	static public String getElementAttribute(Document doc, String tag,
			String attr, String strategy) {

		if (tag.startsWith("TXT[") && tag.endsWith("]")) {
			String tags[] = tag.split(",");
			tags[0] = tags[0].replace("TXT[", "");
			tags[1] = tags[1].replace("]", "");
			String html = doc.html();
			int idxStart = html.indexOf(tags[0]);
			if (idxStart < 0)
				return null;
			int idxEnd = html.indexOf(tags[1], idxStart + tags[0].length());
			if (idxEnd < 0)
				return null;

			return html.substring(idxStart + tags[0].length(), idxEnd);
		}

		Element elmt = getFirstElement(doc, tag);

		if (elmt == null)
			return null;

		String txt = null;
		if ((attr != null) && (attr.length() > 0))
		    txt = elmt.attr(attr);
		else {
		    txt = elmt.text();
		}
		
		if ((strategy != null) && (strategy.length() > 0)) {
		    try {
                Class<?> mClass = Class.forName(strategy);
                ExtractionStrategy strategyObj = (ExtractionStrategy) mClass.newInstance();
                txt = strategyObj.extract(elmt, txt);
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            }
		}
		
		return txt;
	}

	private static void setValues(Document doc, Container<Object> container,
			Object srcUrlPattern, String separator) {

		Class<?> paramClass = (Class<?>) ((ParameterizedType) container
				.getClass().getGenericSuperclass()).getActualTypeArguments()[0];

		Elements elemts = getElements(doc, separator);
		for (Element elemt : elemts) {
			Object element = fromDocument(createDocument(elemt, doc.baseUri()),
					paramClass, srcUrlPattern);
			container.add(element);
		}
	}

	private static void setValue(Document doc, Object result, Field field,
			String str) {
		try {

			if (field.getType() == String.class) {
				field.set(result, str);
			}

			if (field.getType() == Integer.class) {
				field.set(result, parseInteger(str));
			}

			if (field.getType() == Calendar.class) {
				field.set(result, parseCalendar(str));
			}

			if (field.getType() == URL.class) {
				field.set(result, buildURL(doc, str));
			}

			if (field.getType() == Double.class) {
				field.set(result, parseDouble(str));
			}

			if (field.getType() == Medium.class) {
				field.set(result, new Medium(buildURL(doc, str)));
			}

			if (field.getType().getSuperclass() == Container.class) {
				Annotation classAnnotations[] = field.getDeclaredAnnotations();
				@SuppressWarnings("unchecked")
				Container<Object> container = (Container<Object>) field
						.getType().newInstance();
				field.set(result, container);
				for (Annotation classAnnotation : classAnnotations) {
					if (classAnnotation instanceof DocMappings
							|| classAnnotation instanceof HTMLMapping) {

						HTMLMapping mapping = getMatchMapping(classAnnotation,
								doc.baseUri(), result);
						if (mapping != null) {
							setValues(doc, container, result, mapping.tag());
						}
					}
				}
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	private static class MyHTMLMappingComparable implements
			Comparator<HTMLMapping> {

		@Override
		public int compare(HTMLMapping o1, HTMLMapping o2) {
			if (o1.priority() != o2.priority())
				return Integer.valueOf(o2.priority()).compareTo(
						Integer.valueOf(o1.priority()));

			if (o1.version() != o2.version())
				return Integer.valueOf(o2.version()).compareTo(
						Integer.valueOf(o1.version()));

			if (!o1.url().equals(o2.url()))
				return o2.url().compareTo(o1.url());

			return 0;
		}
	}

	private static HTMLMapping getMatchMapping(Object obj, String docUrl,
			Object src) {
		try {
			if (!(obj instanceof DocMappings) && !(obj instanceof HTMLMapping))
				throw new RuntimeException(obj
						+ " is not HTMLMappings or HTMLMapping type");

			List<HTMLMapping> orderedMapping = new ArrayList<HTMLMapping>();

			if (obj instanceof DocMappings)
				orderedMapping.addAll(Arrays.asList(((DocMappings) obj)
						.html()));
			else
				orderedMapping.add((HTMLMapping) obj);

			Collections.sort(orderedMapping, new MyHTMLMappingComparable());

			String url = null;
			Field target = null;
			boolean isAccessible = false;
			for (HTMLMapping mapping : orderedMapping) {
				url = mapping.url();

				if (url.contains("{") && (src != null)) {
					int beginIdx = url.indexOf("{");
					int endIdx = url.indexOf("}", beginIdx + 1);
					String name = url.substring(beginIdx + 1, endIdx);

					Field fieldTags[] = src.getClass().getDeclaredFields();
					for (Field field : fieldTags) {
						if (field.getName().equals(name)) {
							target = field;
							break;
						}
					}

					if (target != null) {
						isAccessible = target.isAccessible();
						target.setAccessible(true);
					}

					url = url.replace("{" + name + "}", target == null ? ""
							: (target.get(src) == null ? "" : target.get(src)
									.toString()));

					if (target != null) {
						target.setAccessible(isAccessible);
					}
				}

				if (docUrl.contains(url))
					return mapping;
			}

			return null;
		} catch (Exception e) {
			return null;
		}
	}

	@SuppressWarnings("unchecked")
	public static <T> T fromDocument(Document document, Class<T> classOfT,
			Object result) {
		try {
			Object initialResult = result;

			if (result == null || result.getClass() != classOfT)
				result = classOfT.newInstance();

			if (document == null)
				return (T) result;

			String docUrl = document.baseUri();

			if (result instanceof Container<?>) {
				for (Annotation annotation : result.getClass().getAnnotations()) {
					if (annotation instanceof DocMappings
							|| annotation instanceof HTMLMapping) {

						HTMLMapping mapping = getMatchMapping(annotation,
								docUrl, initialResult);

						if (mapping != null) {
							setValues(document, (Container<Object>) result,
									initialResult, mapping.tag());
						}
					}
				}
			}

			Field[] fields = classOfT.getDeclaredFields();
			for (Field field : fields) {

				boolean isAccessible = field.isAccessible();
				field.setAccessible(true);

				if (field.get(result) != null) {
					field.setAccessible(isAccessible);
					continue;
				}

				for (Annotation annotation : field.getAnnotations()) {
					if (annotation instanceof DocMappings
							|| annotation instanceof HTMLMapping) {

						HTMLMapping mapping = getMatchMapping(annotation,
								docUrl, result);
						if (mapping != null) {
							String txt = getElementAttribute(document,
									mapping.tag(), mapping.attr(), mapping.strategy());
							setValue(document, result, field, txt);
							field.setAccessible(isAccessible);
							continue;
						}
					}
				}
			}

			return (T) result;
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
}
